# Copyright (c) 2019 Presto Labs Pte. Ltd.
# Author: taekwon (originally from BitmexRestApi, HuobiFuturesNativePrivateClient)

import datetime
import enum
import functools
import hashlib
import hmac
import json
import logging
import sys

import time
import urllib.parse

from typing import Optional
import requests
from requests.auth import AuthBase
from tornado.ioloop import IOLoop

from coin.base.timestamp import get_timestamp
from coin.exchange.deribit_v1.kr_rest.futures_product import DeribitFuturesProduct
from coin.exchange.base.kr_rest.auth_base import AuthKey
from coin.exchange.base.kr_rest.rest_client_base import RestUpdate
from coin.exchange.util.async_http_client import AsyncHttpClient


# 'limit', 'stop_limit', 'market', 'stop_market'
class DeribitFuturesOrderPriceType(enum.Enum):
  LIMIT = 'limit'
  STOP_LIMIT = 'stop_limit'
  MARKET = 'market'
  STOP_MARKET = 'stop_market'
  POST_ONLY = 'post_only'
  REDUCE_ONLY = 'reduce_only'


class DeribitFuturesOffsetType(enum.Enum):
  OPEN = "open"
  CLOSE = "close"


class DeribitFuturesDirectionType(enum.Enum):
  BUY = "buy"
  SELL = "sell"
  ZERO = "zero"


class DeribitFuturesStatusType(enum.Enum):
  OPEN = "open"
  FILLED = "filled"
  REJECTED = "rejected"
  CANCELLED = "cancelled"
  UNTRIGGERED = "untriggered"


class DeribitFuturesAPIKeyAuth(AuthBase):
  def __init__(self, key_file):
    self._key = AuthKey.from_file(key_file)

  def generate_signature(self, method: str, url: str, timestamp: int, nonce: int, data=''):
    # Remove the base
    parsed_url = urllib.parse.urlparse(url)
    path = parsed_url.path
    if parsed_url.query:
      path = path + '?' + parsed_url.query
    if isinstance(data, (bytes, bytearray)):
      data = data.decode('utf8')
    message = f"{timestamp}\n{nonce}\n{method}\n{path}\n{data}\n"
    signature = hmac.new(self._key.secret_key_bytes,
                         bytes(message, 'utf8'),
                         digestmod=hashlib.sha256).hexdigest()
    return signature

  def __call__(self, r):
    timestamp = int(round(time.time() * 1000))
    nonce = int(round((get_timestamp() * 1000)))
    sig = self.generate_signature(r.method, r.url, timestamp, nonce, r.body or '')
    r.headers['Authorization'] = "deri-hmac-sha256 id={},ts={},nonce={},sig={}"\
        .format(self._key.access_key, timestamp, nonce, sig)
    return r


DERIBIT_API_HOST = 'www.deribit.com/api/v2/'


class DeribitFuturesSyncQueryer(object):
  def __init__(
      self,
      key_file,
      *,
      api_host=None,
      disable_ssl=False,
      timeout=10,
  ):
    self._timeout = timeout
    self._auth = DeribitFuturesAPIKeyAuth(key_file)
    scheme = 'http://' if disable_ssl else 'https://'
    self._api_host = scheme + (api_host or DERIBIT_API_HOST)
    self._logger = logging.getLogger(__name__)

  def auth_request(self, method, path, params):
    assert method in ('GET', 'POST'), method
    data = None
    if method == 'POST':
      data = params
      params = None

    url = urllib.parse.urljoin(self._api_host, path)
    response = None
    try:
      response = requests.request(method,
                                  url,
                                  params=params,
                                  json=data,
                                  auth=self._auth,
                                  timeout=self._timeout)
      content = response.json()
      return content
    except requests.exceptions.RequestException:
      self._logger.exception('RequestException: %s')
      raise
    except json.JSONDecodeError:
      self._logger.exception('Json decode: %s', response.content)
      raise
    except Exception as e:
      self._logger.exception('Unknown exception, %s', e)
      raise

  def _query_nocheck(self, method, path, params):
    req_timestamp = time.time()
    content = self.auth_request(method, path, params)
    res_timestamp = time.time()
    status = requests.codes.ok
    update = RestUpdate(req_timestamp, res_timestamp, content, status)
    return update

  def query(self, method, path, params=None):
    return self._query_nocheck(method, path, params)


class DeribitFuturesAsyncQueryer(object):
  def __init__(
      self,
      key_file,
      *,
      api_host=None,
      disable_ssl=False,
      timeout=10,
      max_connections=4,
  ):
    self._timeout = timeout
    self._http_client = AsyncHttpClient(max_connections=max_connections)
    self._auth = DeribitFuturesAPIKeyAuth(key_file)
    scheme = 'http://' if disable_ssl else 'https://'
    self._api_host = scheme + (api_host or DERIBIT_API_HOST)

  def query(self, method, path, params=None):
    assert method in ('GET', 'POST'), method
    data = None
    if method == 'POST':
      data = params
      params = None

    url = urllib.parse.urljoin(self._api_host, path)
    future = self._http_client.fetch(method=method,
                                     url=url,
                                     params=params,
                                     json=data,
                                     auth=self._auth,
                                     timeout=self._timeout)
    return future


class DeribitFuturesNativePrivateClient(object):
  def __init__(self,
               *,
               key_file,
               api_host=None,
               disable_ssl=False,
               timeout=10,
               max_connections=8,
               use_async=False):
    api_host = api_host or DERIBIT_API_HOST
    if use_async:
      self._auth_queryer = DeribitFuturesAsyncQueryer(key_file,
                                                      api_host=api_host,
                                                      disable_ssl=disable_ssl,
                                                      timeout=timeout,
                                                      max_connections=max_connections)
    else:
      self._auth_queryer = DeribitFuturesSyncQueryer(key_file, api_host=api_host, timeout=timeout)

  def query_positions(self, currency):
    update = self._query('GET', 'private/get_positions', params={'currency': currency})
    return update

  def query_account_summary(self, currency):
    update = self._query('GET', 'private/get_account_summary', params={'currency': currency})
    return update

  def query_order_info(self, order_id):
    update = self._query('GET', 'private/get_order_state', params={'order_id': order_id})
    return update

  def query_open_orders(self, currency):
    update = self._query('GET',
                         'private/get_open_orders_by_currency',
                         params={'currency': currency})
    return update

  def query_historical_orders(self, currency, count=20, offset=0, include_unfilled='true'):
    params = {
        'currency': currency,
        'count': count,
        'offset': offset,
        'include_unfilled': include_unfilled
    }
    update = self._query('GET', 'private/get_order_history_by_currency', params=params)
    return update

  def make_order_dict(self,
                      symbol: str,
                      price: float,
                      qty: float,
                      order_type: DeribitFuturesOrderPriceType,
                      trigger_price: Optional[float] = None,
                      trigger_by: Optional[str] = None,
                      post_only: Optional[bool] = None,
                      reduce_only: Optional[bool] = None):
    assert isinstance(order_type, DeribitFuturesOrderPriceType)
    params = {'instrument_name': symbol, 'price': price, 'amount': qty, 'type': order_type.value}

    if post_only:
      params[
          'post_only'] = 'true'  # treat as string because request param doesn't serialize boolean well.
    if reduce_only:
      params['reduce_only'] = 'true'  # same here

    if order_type == 'stop_limit':
      assert trigger_price is not None
      params['stop_price'] = trigger_price
      params['trigger'] = trigger_by or 'last_price'  # 'MarkPrice'

    return params

  def create_order(self,
                   symbol: str,
                   side: DeribitFuturesDirectionType,
                   price: float,
                   qty: float,
                   order_type: Optional[DeribitFuturesOrderPriceType] = None,
                   trigger_price: Optional[float] = None,
                   trigger_by: Optional[str] = None,
                   post_only: Optional[bool] = None,
                   reduce_only: Optional[bool] = None):
    '''
    For detail of each parameter, refer to
      https://docs.deribit.com/v2/?shell#private-buy
    Example:
      create_order(symbol='BTC-PERPETUAL',
                   side='buy',
                   price=8500.5,
                   qty=1000,
                   order_type='limit'
                   post_only=True)
    '''
    params = self.make_order_dict(
        symbol=symbol,
        price=price,
        qty=qty,
        order_type=order_type,
        trigger_price=trigger_price,
        trigger_by=trigger_by,
        post_only=post_only,
        reduce_only=reduce_only,
    )
    method = 'GET'
    url = 'private/{}'.format(side.value)
    update = self._query(method, url, params=params)
    return update

  def make_amend_dict(self,
                      order_id: Optional[str] = None,
                      price: Optional[float] = None,
                      qty: Optional[float] = None,
                      post_only: Optional[bool] = None,
                      trigger_price: Optional[float] = None):
    assert (order_id is not None) and (price is not None) and (qty is not None)
    params = {'order_id': order_id, 'price': price, 'amount': qty}
    if post_only:
      params['post_only'] = 'true'
    if trigger_price is not None:
      params['stop_price'] = trigger_price
    return params

  def amend_order(self,
                  order_id: Optional[str] = None,
                  price: Optional[float] = None,
                  qty: Optional[float] = None,
                  post_only: Optional[bool] = None,
                  trigger_price: Optional[float] = None):
    params = self.make_amend_dict(order_id=order_id,
                                  price=price,
                                  qty=qty,
                                  post_only=post_only,
                                  trigger_price=trigger_price)
    method = 'GET'
    url = 'private/edit'
    update = self._query(method, url, params=params)
    return update

  def cancel_order(self, order_id: Optional[str] = None):
    assert order_id is not None
    params = {'order_id': order_id}
    method = 'GET'
    url = 'private/cancel'
    update = self._query(method, url, params=params)
    return update

  def cancel_all(self):
    method = 'GET'
    url = 'private/cancel_all'
    update = self._query(method, url)
    return update

  def _query(self, method, path, params=None):
    return self._auth_queryer.query(method, path, params=params)


class TestPrivateClient(object):
  def __init__(self, key_filepath, ioloop=None):
    self._key_filepath = key_filepath
    self._ioloop = ioloop
    self._private_client = None
    self._scheduler = None
    self._logger = logging.getLogger(__name__)

  def start(self):
    self._private_client = DeribitFuturesNativePrivateClient(key_file=self._key_filepath,
                                                             use_async=True)

  def on_submit_response(self, orders, future):
    try:
      res = future.result()
      print(res.json())
      print(orders)
      # print(len(res.json()['result']))
    except Exception as e:
      print(e)

  def order(self):
    future = self._private_client.create_order(symbol='BTC-PERPETUAL',
                                               side='buy',
                                               price=1000.1,
                                               qty=500.0,
                                               order_type='limit')
    self._ioloop.add_future(future, functools.partial(self.on_submit_response, ['order1']))

  def cancel(self, order_id):
    future = self._private_client.cancel_order(order_id=order_id)
    self._ioloop.add_future(future, functools.partial(self.on_submit_response, ['order2']))

  def cancel_all(self):
    future = self._private_client.cancel_all()
    self._ioloop.add_future(future, functools.partial(self.on_submit_response, ['order3']))

  def query_account(self):
    future = self._private_client.query_account_summary('BTC')
    self._ioloop.add_future(future, functools.partial(self.on_submit_response, ['order4']))
    self._ioloop.add_timeout(datetime.timedelta(seconds=10), self.query_account)

  def query_position(self):
    future = self._private_client.query_positions('BTC')
    self._ioloop.add_future(future, functools.partial(self.on_submit_response, ['order5']))
    self._ioloop.add_timeout(datetime.timedelta(seconds=10), self.query_position)

  def query_open_orders(self):
    future = self._private_client.query_open_orders('BTC')
    self._ioloop.add_future(future, functools.partial(self.on_submit_response, ['order6']))
    self._ioloop.add_timeout(datetime.timedelta(seconds=10), self.query_open_orders)

  def query_historical_orders(self):
    future = self._private_client.query_historical_orders('BTC')
    self._ioloop.add_future(future, functools.partial(self.on_submit_response, ['order7']))
    self._ioloop.add_timeout(datetime.timedelta(seconds=10), self.query_historical_orders)


def main(argv):
  key_filepath = '/home/taekwon/deribit_api_key.json'

  product = DeribitFuturesProduct.FromStrNativeProduct('BTC-PERPETUAL')
  print(product.symbol)
  print(product.native_symbol)
  print(product.base.currency)
  print(product.base.native_currency)
  # print(product.base)

  ioloop = IOLoop.current()
  test_client = TestPrivateClient(key_filepath, ioloop)
  test_client.start()
  # test_client.order()
  # test_client.cancel('2627117610')
  # test_client.cancel_all()
  # test_client.query_open_orders()
  # test_client.query_position()
  # test_client.query_account()
  test_client.query_historical_orders()
  ioloop.start()


if __name__ == '__main__':
  logging.basicConfig(level='INFO', format='%(levelname)s %(asctime)s %(name)s] %(message)s')
  sys.exit(main(sys.argv))
